記得我們 Day3 提到要自動綁定 ChatClient 卻失敗吧,今天來看看如何解決
要初始化 ChatClient 只能使用 Builder 建立
@RestController
class MyController {
private final ChatClient chatClient;
public MyController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
}
若有其他 Component 會使用 AI,可以在 Config 類別建立 Bean,要使用的 Component 在自動綁定即可
@Configuration
class Config {
@Bean
ChatClient chatClient(ChatClient.Builder builder) {
return builder.build();
}
}
其實凱文大叔一直覺得之後的改版一定會讓 ChatClient 可以自動綁定
前面我們 ChatModel 也用得好好的,為何在講記憶之前要先說 ChatClient ?
在 Spring 框架中,結尾冠上 Client 的類別,就是更高級的封裝,例如:RestClient、WebClient、JdbcClient等,而且這一系列的類別都提供 Fluent 風格的 API,雖然 ChatModel 也提供一部分的 Fluent API,不過僅限於類別本身的參數,許多外部設定還是得先建立其他物件在引入
ChatClient 最大的差別就是將這些外部類別設定的資料也一起整合進來,下面就來比較 ChatModel 與 ChatClient 寫法的差異吧
ChatModel:
ChatResponse response = chatModel
.call(new Prompt(
new UserMessage("Tell me a joke"),
));
ChatClient:
ChatResponse chatResponse = chatClient.prompt()
.user("Tell me a joke")
.call()
.chatResponse();
這個範例看起來雖然程式碼差不多,不過可以看到 ChatClient 在設定 Prompt 時直接使用 .prompt(),ChatModel 卻要自己 new 一個,這就是兩者最大的差別
ChatModel:
@GetMapping("/template")
public String template1(@RequestParam String llm) {
String template = "請問{llm}目前有哪些模型,各有甚麼特殊能力";
PromptTemplate promptTemplate = new PromptTemplate(template);
Prompt prompt = promptTemplate.create(Map.of("llm", llm));
ChatResponse response = chatModel.call(prompt);
return response.getResult().getOutput().getContent();
}
ChatClient:
@GetMapping("/template")
public String template(@RequestParam String llm) {
String template = "請問{llm}目前有哪些模型,各有甚麼特殊能力";
ChatResponse response = chatClient.prompt()
.user(u -> u.text(template)
.param("llm", llm)
)
.call()
.chatResponse();
return response.getResult().getOutput().getContent();
}
這個例子可以看出 ChatClient 在設定 Message 時直接使用 Lambda 語法建立 PromptTemplate 並使用 param 帶入參數,一氣呵成
ChatModel:
record ActorsFilms(String actor, List<String> movies) {};
@GetMapping("/films")
public ActorsFilms films(String actor) {
String template = """
列出演員{actor}最有名的五部電影,需用繁體中文回答
{format}
""";
BeanOutputConverter<ActorsFilms> beanOutputConverter =
new BeanOutputConverter<>(ActorsFilms.class);
String format = beanOutputConverter.getFormat();
Generation generation = chatModel.call(
new Prompt(new PromptTemplate(template, Map.of("actor", actor, "format", format)).createMessage())).getResult();
ActorsFilms actorsFilms = beanOutputConverter.convert(generation.getOutput().getContent());
return actorsFilms;
}
ChatClient:
record ActorsFilms(String actor, List<String> movies) {};
@GetMapping("/films")
public ActorsFilms films(String actor) {
String template = """
列出演員{actor}最有名的五部電影,需用繁體中文回答
""";
ActorFilms actorFilms = chatClient.prompt()
.user(u -> u.text(template).param("actor", actor))
.call()
.entity(ActorFilms.class);
return actorFilms;
}
結構化輸出時 ChatClient 竟然只用 .entity() 就取代 BeanOutputConverter 越複雜的案例使用 ChatClient 就會讓程式碼更簡潔,ChatClient 其實是將 BeanOutputConverter 包進類別裡處理結構化輸出
因為 ChatClient 把 .prompt() 加入 Fluent API 中,原本要使用 Options 設定的 Function 的部分也跟著一起簡化了
ChatModel
@GetMapping("/func")
public String func(String prompt) {
return chatModel.call(
new Prompt(prompt,
OpenAiChatOptions.builder()
// Funciton可以放多筆,也能依據 API 接口放上合適的 Function
.withFunction("ProductSalesInfo")
.withFunction("ProductDetailsInfo")
.build())
).getResult().getOutput().getContent();
}
ChatClient
@GetMapping("/func")
public String func(String prompt) {
return chatClient.prompt().
.user(prompt)
.functions("ProductSalesInfo","ProductDetailsInfo")
.call().content();
}
Function 數量若不多,甚至能省略在 Config 中建立 Bean 的部分( 設定 Bean 的方式請參考 Day11),直接將 name、description 和 function 的實體一起放在 chatClient 中,如下面的程式碼
@GetMapping("/func")
public String func(String prompt) {
return chatClient.prompt().
.user(prompt)
.function("CurrectDateTime","Get the Date Time",new CurrectDateTimeFunction())
.call().content();
}
在 ChatClient 中還能設定 Default 參數,這部分需要在 builder 時設定,之後執行時沒帶入參數就會使用預設的內容,舉個實際例子
@Configuration
class Config {
@Bean
ChatClient chatClient(ChatClient.Builder builder) {
return builder.defaultSystem("你是個友善的聊天機器人,不管問甚麼問題都盡可能提供答案")
.build();
}
}
之後呼叫 chatClient
時若沒使用 .system()
覆蓋 SystemMessage
,Spring AI 就會使用 defaultSystem
的內容來作為SystemMessage
下面是預設的方法以及正常呼叫的方法
Default | Standard |
---|---|
defaultSystem | system |
defaultUser | user |
defaultFunction | function |
defaultFunctions | functions |
defaultOptions | options |
defaultAdvisors | advisors |
有注意到 advisors
嗎?這是前面沒提過的內容,也會是後面記憶跟 RAG 最重要的調用方法,就留在後面慢慢說明囉
今天學到的內容:
今天以後程式碼的部分會盡量使用 ChatClient 來撰寫,有了前面幾天的基礎在看到簡化後的程式碼應該會更容易上手
今日程式碼: https://github.com/kevintsai1202/SpringBoot-AI-Day15.git
凱文大叔使用 Java 開發程式超過 20 年,對於 Java 生態非常熟悉,曾使用反射機制開發 ETL 框架,對 Spring 背後的原理非常清楚,目前以 Spring Boot 作為後端開發框架,前端使用 React 搭配 Ant Design
下班之餘在 Amazing Talker 擔任程式語言講師,並獲得學員的一致好評
最近剛成立一個粉絲專頁-凱文大叔教你寫程式 歡迎大家多追蹤,我會不定期分享實用的知識以及程式開發技巧
想討論 Spring 的 Java 開發人員可以加入 FB 討論區 Spring Boot Developer Taiwan
我是凱文大叔,歡迎一起加入學習程式的行列